Skip to content

Conversation

@cywin1018
Copy link
Contributor

@cywin1018 cywin1018 commented Sep 7, 2025

๐Ÿš€ ํ’€ ๋ฆฌํ€˜์ŠคํŠธ ์ œ์•ˆ

  • ๋ฒ„๊ทธ ์ˆ˜์ •

โœˆ๏ธ ๊ด€๋ จ ์ด์Šˆ

๐Ÿ“‹ ์ž‘์—… ๋‚ด์šฉ

qa์— ๋‚ด์šฉ ์ผ๋ถ€ ์ˆ˜์ •.
๋…ธ์…˜์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

๐Ÿ“ธ ์Šคํฌ๋ฆฐ์ƒท (์„ ํƒ ์‚ฌํ•ญ)

๐Ÿ“„ ๊ธฐํƒ€

Summary by CodeRabbit

  • New Features

    • ์ฑ„์šฉ ์นด๋“œ ํด๋ฆญ ์‹œ ์ƒ์„ธ ๋ชจ๋‹ฌ๋กœ ๋‚ด์šฉ ํ™•์ธ ๊ฐ€๋Šฅ; ์ข‹์•„์š” ๋™์ž‘์€ ๋…๋ฆฝ ์œ ์ง€.
    • ์ปค๋ฎค๋‹ˆํ‹ฐ HOT ํ•ญ๋ชฉ ํด๋ฆญ์œผ๋กœ ์ƒ์„ธ ์ด๋™ ๋ฐ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ํ‘œ์‹œ.
    • ํ• ์ผ์„ โ€œ๋‚ด ํ• ์ผโ€์— ์ถ”๊ฐ€/์ทจ์†Œ ๊ฐ€๋Šฅ, ์ €์žฅ ์ˆ˜ ํ‘œ์‹œ ๋ฐ ํ† ์ŠคํŠธ ์•ˆ๋‚ด ์ œ๊ณต.
    • ํ• ์ผ ๊ฐ€์ ธ์˜ค๊ธฐ ์ด๋ฒคํŠธ๋ฅผ ์™ธ๋ถ€๋ถ„์„(Amplitude)์œผ๋กœ ์ „์†กํ•ด ์‚ฌ์šฉ ํ–‰์œ„ ์ถ”์ .
  • Style

    • ์ฒดํฌ๋ฆฌ์ŠคํŠธ์˜ ๋ถˆํ•„์š”ํ•œ ์•„์ด์ฝ˜ ๋ฐฐ์ง€ ์ œ๊ฑฐ.
    • HOT ๋ชฉ๋ก ์„ค๋ช…์— ๋ง์ค„์ž„ ์ ์šฉํ•ด ๊ฐ€๋…์„ฑ ๊ฐœ์„ .
  • Bug Fixes

    • ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ null ํ—ˆ์šฉ์œผ๋กœ ๊ด€๋ จ ์˜ค๋ฅ˜ ๋ฐฉ์ง€ ๋ฐ ์•ˆ์ •์„ฑ ํ–ฅ์ƒ.

@cywin1018 cywin1018 requested a review from Chasyuss September 7, 2025 06:22
@coderabbitai
Copy link

coderabbitai bot commented Sep 7, 2025

Walkthrough

์ฒดํฌ๋ฆฌ์ŠคํŠธ์˜ ๋น„ํŽธ์ง‘ ํ•ญ๋ชฉ์—์„œ AddIcon ๋ฐฐ์ง€๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ์ปค๋ฎค๋‹ˆํ‹ฐ HOT ๋ฆฌ์ŠคํŠธ์— ์ด๋ฏธ์ง€ ํด๋ฐฑ ๋ฐ ํ•ญ๋ชฉ ํด๋ฆญ ๋„ค๋น„๊ฒŒ์ด์…˜์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌํฌ๋ฃจํŠธ ์นด๋“œ์— ๋ชจ๋‹ฌ ์ƒ์„ธ๋ณด๊ธฐ์™€ ์„ ํƒ/๋‹ซ๊ธฐ ์ƒํƒœ๋ฅผ ๋„์ž…ํ–ˆ๊ณ , ํƒ€ ์ปดํฌ๋„ŒํŠธ์— ํ• ์ผ ์ถ”๊ฐ€ ํŠธ๋ž˜ํ‚น(Amplitude)๊ณผ ๋ถ๋งˆํฌ ํ† ๊ธ€ยทํ† ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž… ๋ฐ ์Šคํ‚ค๋งˆ์— ์†Œ๊ทœ๋ชจ ํ•„๋“œ ๋ณ€๊ฒฝ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

Changes

Cohort / File(s) Summary
Checklist UI cleanup
src/common/CheckList.tsx
๋น„ํŽธ์ง‘ ๋ชจ๋“œ์—์„œ AddIcon ๋ฐ ๋ฐฐ์ง€ ์ œ๊ฑฐ; BookMarkIcon ์œ ์ง€; ์†Œ์†Œํ•œ ๊ณต๋ฐฑ/์ฃผ์„ ๋ณ€๊ฒฝ.
Community HOT list interactions
src/pages/community/components/CommunityLeftSide.tsx
BaseImage ํด๋ฐฑ ์‚ฌ์šฉ (`src={item.imageUrl
Recruit card detail modal
src/pages/home/components/HomeRecruit.tsx
์นด๋“œ ํด๋ฆญ์œผ๋กœ convertToRecruitItem ์ƒ์„ฑ ํ›„ selectedCard/isModalOpen๋กœ ๋ชจ๋‹ฌ(CardDetail) ์˜คํ”ˆยท๋‹ซ๊ธฐ ๊ตฌํ˜„; ์ข‹์•„์š” ์˜์—ญ์€ ์ด๋ฒคํŠธ ์ „ํŒŒ ์ฐจ๋‹จ.
Other Todo bookmarking & toast
src/pages/otherTodoList/components/OtherTodoCard.tsx
ํ•ญ๋ชฉ๋ณ„ ์ถ”๊ฐ€/์‚ญ์ œ ๋ฎคํ…Œ์ด์…˜ ์—ฐ๋™, ๋กœ์ปฌ ์ถ”๊ฐ€ ์ƒํƒœ ๋ฐ saveCount/isSaved optional ํ•„๋“œ ๋„์ž…, ํ† ์ŠคํŠธ(์•ฝ 2.5s) UI ๋ฐ ์•Œ๋ฆผ/์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€, Amplitude ํ˜ธ์ถœ ํฌํ•จ.
Amplitude utility
src/utils/amplitude.ts
๋ธŒ๋ผ์šฐ์ € ์ปจํ…์ŠคํŠธ์—์„œ ๋™์ž‘ํ•˜๋Š” trackTodoImport(todoTitle: string) ์ถ”๊ฐ€ โ€” ํŽ˜์ด์ง€ ๋งคํ•‘, ์ด๋ฒคํŠธ ํŽ˜์ด๋กœ๋“œ(toto_length ๋“ฑ) ์ „์†ก ๋ฐ ์ฝ˜์†” ๋กœ๊ทธ.
Community add tracking
src/pages/community/components/CommunityContents.tsx
toggleAdd(id, isAdded) โ†’ toggleAdd(id, isAdded, todoTitle)๋กœ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ํ™•์žฅ; ์ถ”๊ฐ€ ๊ฒฝ๋กœ์—์„œ trackTodoImport(todoTitle) ํ˜ธ์ถœ ์ถ”๊ฐ€.
Job detail add tracking
src/pages/jobDetail/components/TabContent/...
SproutContent.tsx, TreeContents.tsx
์ถ”๊ฐ€ ํ•ธ๋“ค๋Ÿฌ์— title ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€ ๋ฐ trackTodoImport(title) ํ˜ธ์ถœ ์‚ฝ์ž…; onClick ํ˜ธ์ถœ๋ถ€์— ์ œ๋ชฉ ์ „๋‹ฌ.
Type / Hook updates
src/hook/useHomeQuery.ts, src/hook/community/query/useGetHotPopularQuery.ts
RecruitProps์— url: string ์ถ”๊ฐ€; HotPopularItem์— todoGroupId: number ์ถ”๊ฐ€ ๋ฐ ์ฟผ๋ฆฌ ๋‚ด ๋””๋ฒ„๊ทธ console.log ์ œ๊ฑฐ.
Schema update
src/validation/home/popularSchema.ts
PopularTodoSchema.profileImage๋ฅผ z.string().url().nullable()๋กœ ๋ณ€๊ฒฝ(ํƒ€์ž…์ด `string
Navigation tweak
src/pages/home/components/HomeDreamer.tsx
๋ฒ„ํŠผ ๋ผ์šฐํŠธ /jobfound โ†’ /community๋กœ ๋ณ€๊ฒฝ.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant HomeRecruit as HomeRecruit
  participant Modal as CardDetail

  User->>HomeRecruit: ๋ฆฌํฌ๋ฃจํŠธ ์นด๋“œ ํด๋ฆญ
  HomeRecruit->>HomeRecruit: convertToRecruitItem(data)
  HomeRecruit->>HomeRecruit: selectedCard ์„ค์ •, isModalOpen=true
  HomeRecruit->>Modal: CardDetail ๋ Œ๋”(item, onClose)
  User-->>Modal: ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ
  Modal-->>HomeRecruit: onClose()
  HomeRecruit->>HomeRecruit: isModalOpen=false
  note right of HomeRecruit: ์ข‹์•„์š” ์˜์—ญ์€ stopPropagation์œผ๋กœ ์นด๋“œ ์—ด๋ฆผ ๋ฐฉ์ง€
Loading
sequenceDiagram
  autonumber
  actor User
  participant Community as CommunityContents / OtherTodoCard
  participant API as Add/Delete ๋ฎคํ…Œ์ด์…˜
  participant Amplitude as trackTodoImport
  participant Toast as ToastModal

  User->>Community: "๋‚ด ํ• ์ผ์— ์ถ”๊ฐ€" ํด๋ฆญ (todoTitle)
  alt not added
    Community->>Amplitude: trackTodoImport(todoTitle)
    Community->>API: addTodo(id)
    API-->>Community: success / error
  else already added
    Community->>API: deleteTodo(id)
    API-->>Community: success / error
  end
  alt success
    Community->>Community: ๋กœ์ปฌ ์ƒํƒœ/์นด์šดํŠธ ์—…๋ฐ์ดํŠธ
    Community->>Toast: show(๋ฉ”์‹œ์ง€)
  else error
    Community->>User: alert(์˜ค๋ฅ˜)
  end
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~45 minutes

Possibly related PRs

Suggested labels

๐Ÿ›Bug, โœจFeature

Suggested reviewers

  • Chasyuss

Poem

์˜ค๋Š˜๋„ ๊นก์ด ์ฝ”๋“œ๋ฐญ์„ ๋›ฐ์–ด๋“ค์–ด
๋ฐฐ์ง€๋Š” ํˆญ, UI๋Š” ๋” ๊ฐ€๋ฒผ์›Œ์กŒ๋„ค
ํด๋ฆญ ํ•œ ๋ฒˆ์— ๋ชจ๋‹ฌ๋กœ ๊ฟˆ์ด ํŽด์ง€๊ณ 
ํ• ์ผ์€ ์™โ€”๋ถ๋งˆํฌ๋กœ ๋“ค์–ด์˜ค๋„ค ๐Ÿฅ•
(\_/) (โ€ขแดฅโ€ข)โœจ

โœจ Finishing Touches
  • ๐Ÿ“ Generate Docstrings
๐Ÿงช Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch qa-330

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cywin1018 cywin1018 changed the title Qa 330 [๐Ÿ›BUG] QA ์‹œ๋ฆฌ์ฆˆ Sep 7, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
src/hook/useHomeQuery.ts (1)

50-59: RecruitProps.url ํ•„๋“œ ์ฒ˜๋ฆฌ ๊ฐ•ํ™” ํ•„์š”
HomeRecruit.tsx์˜ convertToRecruitItem(49โ€“57ํ–‰)์—์„œ data.url || ''๋กœ ๋นˆ ๋ฌธ์ž์—ด ๋Œ€์ฒด๋งŒ ํ•˜๊ณ  ์žˆ์–ด, ๋ฐฑ์—”๋“œ๊ฐ€ url์„ ๋ˆ„๋ฝํ•˜๋ฉด ๋นˆ ๋งํฌ๊ฐ€ ๋…ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์˜ต์…˜ A: src/hook/useHomeQuery.ts์˜ RecruitProps์—์„œ url?: string์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ , ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ.
  • ์˜ต์…˜ B(๊ถŒ์žฅ): zod ์Šคํ‚ค๋งˆ(RecruitItemSchema)๋กœ ์‘๋‹ต์„ ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆํ•˜๊ณ , ๋ˆ„๋ฝ ์‹œ ๋ช…์‹œ์  fallback(์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋˜๋Š” ๊ธฐ๋ณธ URL) ์ ์šฉ.
๐Ÿงน Nitpick comments (13)
src/pages/home/components/HomeDreamer.tsx (1)

41-47: ์ ‘๊ทผ์„ฑ ๊ฐœ์„ : <div onClick> ๋Œ€์‹  <Link> ๋˜๋Š” <button> ์‚ฌ์šฉ ๊ถŒ์žฅ
src/common/Header.tsx(26)์—์„œ โ€˜/communityโ€™ ๊ฒฝ๋กœ๊ฐ€ ์ด๋ฏธ ์ •์˜๋˜์–ด ์žˆ์–ด ์˜๋„ํ•œ ๋ชฉ์ ์ง€์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค; HomeDreamer.tsx(43)์—์„œ ํด๋ฆญ ๊ฐ€๋Šฅํ•œ ์š”์†Œ๋ฅผ <Link to="/community"> ๋˜๋Š” <button>์œผ๋กœ ๊ต์ฒดํ•˜์„ธ์š”.

src/common/CheckList.tsx (1)

75-86: Amplitude ์ด๋ฒคํŠธ๋ช…๊ณผ ๋กœ๊ทธ ๋ฉ”์‹œ์ง€ ๋ถˆ์ผ์น˜

track์€ 'todo_create'๋กœ ๋ณด๋‚ด๋Š”๋ฐ, console.log๋Š” 'todo_create_attempt'๋กœ ๊ธฐ์žฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋””๋ฒ„๊น… ํ˜ผ์„ ์„ ์ค„์ด๋ ค๋ฉด ์ผ์น˜์‹œํ‚ค๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ • ์ œ์•ˆ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

-        console.log('Amplitude event sent: todo_create_attempt (inpage)');
+        console.log('Amplitude event sent: todo_create (inpage)');
src/pages/community/components/CommunityLeftSide.tsx (3)

56-59: onError ํด๋ฐฑ ์ฒ˜๋ฆฌ ์ œ์•ˆ

src์— falsy๊ฐ€ ์•„๋‹Œ ์ž˜๋ชป๋œ URL์ด ์˜ฌ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•ด onError๋กœ ํด๋ฐฑ ์ด๋ฏธ์ง€๋ฅผ ๊ฐ•์ œํ•˜๋Š” ํŽธ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

-                  <img
-                    src={item.imageUrl || BaseImage}
+                  <img
+                    src={item.imageUrl || BaseImage}
+                    onError={(e) => ((e.currentTarget.src = BaseImage))}
                     alt="ํ”„๋กœํ•„์ด๋ฏธ์ง€"

63-67: ๋ง์ค„์ž„ ํ…์ŠคํŠธ ํžŒํŠธ ์ œ๊ณต

truncate ์ ์šฉ์€ ์ข‹์Šต๋‹ˆ๋‹ค. ์ „์ฒด ๋‚ด์šฉ์„ title ์†์„ฑ์œผ๋กœ ๋…ธ์ถœํ•˜๋ฉด ์ ‘๊ทผ์„ฑ๊ณผ ๊ฐ€๋…์„ฑ์ด ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

-                      <div className="w-[170px] min-w-0 flex-1 truncate text-black font-B01-SB">
-                        {item.description}
-                      </div>
+                      <div
+                        className="w-[170px] min-w-0 flex-1 truncate text-black font-B01-SB"
+                        title={item.description}
+                      >
+                        {item.description}
+                      </div>

49-51: ๋ฆฌ์ŠคํŠธ ํ–‰ ์ „์ฒด ํด๋ฆญ ๋„ค๋น„๊ฒŒ์ด์…˜ โ€” ๋ผ์šฐํŠธ ํ™•์ธ ์™„๋ฃŒ ๋ฐ ์ ‘๊ทผ์„ฑ ๋ณด์™„ ๊ถŒ์žฅ
navigate(/otherslist/${item.id}) ํ˜ธ์ถœ์€ src/route/Router.tsx:72์˜ path="/otherslist/:todoGroupId" ๊ฒฝ๋กœ์™€ ์ผ์น˜ํ•จ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. div ๋Œ€์‹  <Link>/<button> ์‚ฌ์šฉ ๋˜๋Š” role="button" + onKeyDown(Enter/Space) ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•ด ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ์„ ๋ณด์™„ํ•˜์„ธ์š”.

src/pages/home/components/HomeRecruit.tsx (4)

14-33: ๋กœ์ปฌ RecruitData ์ค‘๋ณต ์ •์˜ โ†’ ๊ณต์šฉ DTO๋กœ ํ†ตํ•ฉ ์ œ์•ˆ

API ์‘๋‹ต ํ˜•ํƒœ๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์žฌ์ •์˜ํ•˜๋ฉด ๋“œ๋ฆฌํ”„ํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. validation/recruit ํ˜น์€ hook ๋‹จ์˜ ํƒ€์ž…/์Šคํ‚ค๋งˆ๋กœ ์Šน๊ฒฉํ•˜์—ฌ ๋‹จ์ผ ์ถœ์ฒ˜(SOT)๋กœ ๊ด€๋ฆฌํ•˜์„ธ์š”.

์›ํ•˜์‹œ๋ฉด ๊ธฐ์กด ์Šคํ‚ค๋งˆ(RecruitItem)์™€ ๋งคํ•‘๋˜๋Š” DTO ํƒ€์ž… ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜๋Š” PR ๋ณด์กฐ ํŒจ์น˜๋ฅผ ๋“œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


39-41: ๋ชจ๋‹ฌ ์ข…๋ฃŒ ์‹œ ์„ ํƒ ์ƒํƒœ ์ •๋ฆฌ

๋‹ซ๊ธฐ ์‹œ selectedCard๋„ null๋กœ ๋ฆฌ์…‹ํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ/์ƒํƒœ ์ผ๊ด€์„ฑ์ด ์ข‹์•„์ง‘๋‹ˆ๋‹ค.

-            onClose={() => {
+            onClose={() => {
               console.log('Closing modal');
               setIsModalOpen(false);
+              setSelectedCard(null);
             }}

73-80: ๋””๋ฒ„๊ทธ ๋กœ๊ทธ ์ œ๊ฑฐ ์ œ์•ˆ

๋‚จ์•„์žˆ๋Š” console.log๋Š” ๋ฐฐํฌ ์ „์— ์ •๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

-  console.log('Card clicked:', data);
   const recruitItem = convertToRecruitItem(data);
-  console.log('Converted item:', recruitItem);
   setSelectedCard(recruitItem);
   setIsModalOpen(true);
-  console.log('Modal should open');

160-170: ๋ชจ๋‹ฌ ์ ‘๊ทผ์„ฑ ๊ฐœ์„ (ํฌ์ปค์Šค ํŠธ๋žฉ/์Šคํฌ๋กค ๋ฝ)

์˜ค๋ฒ„๋ ˆ์ด๋Š” ์ž˜ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋งŒ ํฌ์ปค์Šค ํŠธ๋žฉ, ESC ๋‹ซ๊ธฐ, body ์Šคํฌ๋กค ๋ฝ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ ‘๊ทผ์„ฑ์ด ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค. Modal ์ปดํฌ๋„ŒํŠธ๋ฅผ ์žฌ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๊ทธ ๋‚ด๋ถ€์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

src/pages/otherTodoList/components/OtherTodoCard.tsx (4)

111-114: ๋ฆฌ์ŠคํŠธ key์— index ์‚ฌ์šฉ ์ง€์–‘

item.todoId์ฒ˜๋Ÿผ ์•ˆ์ •์ ์ธ ๊ณ ์œ ๊ฐ’์„ key๋กœ ์‚ฌ์šฉํ•˜์„ธ์š”. ์žฌ์ •๋ ฌ/์ถ”๊ฐ€/์‚ญ์ œ ์‹œ ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์žฌ์‚ฌ์šฉ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

-            <li
-              key={index}
+            <li
+              key={item.todoId}

145-155: ๋ณ€๊ฒฝ ์ค‘ ๋น„ํ™œ์„ฑํ™” ๋ฐ ์‹œ๊ฐ์  ์ƒํƒœ ์ œ๊ณต

์š”์ฒญ ์ค‘์—๋Š” ๋ฒ„ํŠผ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜์—ฌ ์ค‘๋ณต ํด๋ฆญ์„ ๋ฐฉ์ง€ํ•˜์„ธ์š”. aria-busy๋กœ ์ƒํƒœ ์ „๋‹ฌ๋„ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

-                <button
+                <button
                   type="button"
-                  onClick={() => toggleAdd(item.todoId, isAdded)}
+                  onClick={() => toggleAdd(item.todoId, isAdded)}
+                  disabled={isMutating}
                   className={
                     isAdded
-                      ? 'p-2 text-purple-500 font-B03-SB'
-                      : 'flex items-center justify-center rounded-[10px] bg-purple-500 p-2 text-purple-50 font-B03-SB'
+                      ? 'p-2 text-purple-500 opacity-80 disabled:opacity-50 font-B03-SB'
+                      : 'flex items-center justify-center rounded-[10px] bg-purple-500 p-2 text-purple-50 disabled:opacity-50 font-B03-SB'
                   }
                 >

137-144: saveCount ์ฆ‰์‹œ ๋ฐ˜์˜ ์—ฌ๋ถ€ ํ™•์ธ

invalidate๋กœ ์ƒˆ๋กœ๊ณ ์นจ๋˜๋ฉด OK์ž…๋‹ˆ๋‹ค. ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์ด ํ•„์š”ํ•˜๋ฉด ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ๋กœ saveCount๋ฅผ +1/-1 ์กฐ์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์Šต๋‹ˆ๋‹ค.


162-170: ํ† ์ŠคํŠธ ์ ‘๊ทผ์„ฑ(์Šคํฌ๋ฆฐ๋ฆฌ๋”) ๊ฐœ์„ 

aria-live="polite"์™€ role="status"๋ฅผ ๋ถ€์—ฌํ•˜๋ฉด ํ™”๋ฉด ์ฝ๊ธฐ ํ”„๋กœ๊ทธ๋žจ์—์„œ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ธ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

-        <div className="fixed inset-0 z-50 flex items-center justify-center">
+        <div
+          className="fixed inset-0 z-50 flex items-center justify-center"
+          role="status"
+          aria-live="polite"
+        >
๐Ÿ“œ Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 108ed15 and 7453db5.

๐Ÿ“’ Files selected for processing (7)
  • src/common/CheckList.tsx (1 hunks)
  • src/hook/useHomeQuery.ts (1 hunks)
  • src/pages/community/components/CommunityLeftSide.tsx (2 hunks)
  • src/pages/home/components/HomeDreamer.tsx (1 hunks)
  • src/pages/home/components/HomeRecruit.tsx (3 hunks)
  • src/pages/otherTodoList/components/OtherTodoCard.tsx (3 hunks)
  • src/validation/home/popularSchema.ts (1 hunks)
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (2)
src/pages/home/components/HomeRecruit.tsx (2)
src/store/useUserStore.ts (1)
  • useUserStore (19-36)
src/store/filterStore.ts (1)
  • useFilterStore (37-66)
src/pages/otherTodoList/components/OtherTodoCard.tsx (2)
src/hook/community/useCommunityAddTodoMutation.ts (1)
  • useCommunityAddTodoMutation (16-51)
src/hook/community/useDeleteCommunityTodos.ts (1)
  • useDeleteCommunityTodosMutation (8-38)
๐Ÿ”‡ Additional comments (5)
src/pages/community/components/CommunityLeftSide.tsx (1)

8-9: ์ด๋ฏธ์ง€ ํด๋ฐฑ ์ ์šฉ LGTM

BaseImage ํด๋ฐฑ ์ถ”๊ฐ€๋Š” NPE/๋นˆ URL ์ผ€์ด์Šค์— ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

src/pages/home/components/HomeRecruit.tsx (2)

11-13: CardDetail + RecruitItem ์—ฐ๋™ ๋„์ž… LGTM

์ƒ์„ธ ๋ชจ๋‹ฌ๋กœ์˜ ์ „ํ™˜ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๋ฉฐ, ํƒ€์ž… ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฐ๊ฒฐํ•œ ์ ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


126-134: ์ข‹์•„์š” ์˜์—ญ์˜ ์ด๋ฒคํŠธ ์ „ํŒŒ ์ฐจ๋‹จ LGTM

์นด๋“œ ํด๋ฆญ๊ณผ ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก stopPropagation ์ฒ˜๋ฆฌํ•œ ์  ์ข‹์Šต๋‹ˆ๋‹ค.

src/pages/otherTodoList/components/OtherTodoCard.tsx (2)

6-11: ๋ถ๋งˆํฌ/ํ† ์ŠคํŠธ ๋„์ž… LGTM

UI ํ”ผ๋“œ๋ฐฑ ์š”์†Œ(์•„์ด์ฝ˜/ํ† ์ŠคํŠธ) ์ถ”๊ฐ€๊ฐ€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.


17-19: saveCount/isSaved ํ•„๋“œ ๋„์ž… โ€” ๋ฐ์ดํ„ฐ ์›์ฒœ ํ™•์ธ

๋‘ ํ•„๋“œ๊ฐ€ API์—์„œ ํ•ญ์ƒ ๋‚ด๋ ค์˜ค๋Š”์ง€(ํ˜น์€ ์„ ํƒ์ ) ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋ˆ„๋ฝ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์œผ๋ฉด ์ƒ์œ„ ์Šคํ‚ค๋งˆ/ํƒ€์ž…๋„ optional๋กœ ๋งž์ถ”๊ณ  ์•ˆ์ „ํ•œ ๊ธฐ๋ณธ๊ฐ’์„ ์œ ์ง€ํ•˜์„ธ์š”.

Comment on lines +49 to +71
const convertToRecruitItem = (data: RecruitData): RecruitItem => {
return {
url: data.url || '',
active: data.active || 1,
title: data.title || '',
jobName: data.jobName || '',
companyName: data.companyName || '',
locationName: data.locationName || null,
jobTypeName: data.jobTypeName || '',
experienceLevel: data.experienceLevel || '',
requiredEducationLevel: data.requiredEducationLevel || '',
closeType: data.closeType || '',
salary: data.salary || '',
id: String(data.id),
postTimestamp: data.postTimestamp || '',
postDate: data.postDate || '',
'expiration-timestamp': data['expiration-timestamp'] || '',
'expiration-date': data['expiration-date'] || '',
deadline: data.deadline || '',
count:
typeof data.count === 'string' ? parseInt(data.count) : data.count || 0,
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue

ํ™œ์„ฑ ์ƒํƒœ(active) 0์ด 1๋กœ ์˜ค์ธ๋  ์ˆ˜ ์žˆ์Œ + ์ •์ˆ˜ ํŒŒ์‹ฑ radix ๋ช…์‹œ

  • active: data.active || 1์€ 0(๋น„ํ™œ์„ฑ)์„ 1๋กœ ๋ฎ์–ด์จ ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. nullish ๋ณ‘ํ•ฉ(??)์„ ์“ฐ์„ธ์š”.
  • count: parseInt ์‚ฌ์šฉ ์‹œ 10์ง„์ˆ˜ radix๋ฅผ ๋ช…์‹œํ•˜์„ธ์š”.

์ ์šฉ ์ œ์•ˆ:

   const convertToRecruitItem = (data: RecruitData): RecruitItem => {
     return {
       url: data.url || '',
-      active: data.active || 1,
+      active: data.active ?? 1,
       title: data.title || '',
       jobName: data.jobName || '',
       companyName: data.companyName || '',
       locationName: data.locationName || null,
       jobTypeName: data.jobTypeName || '',
       experienceLevel: data.experienceLevel || '',
       requiredEducationLevel: data.requiredEducationLevel || '',
       closeType: data.closeType || '',
       salary: data.salary || '',
       id: String(data.id),
       postTimestamp: data.postTimestamp || '',
       postDate: data.postDate || '',
       'expiration-timestamp': data['expiration-timestamp'] || '',
       'expiration-date': data['expiration-date'] || '',
       deadline: data.deadline || '',
       count:
-        typeof data.count === 'string' ? parseInt(data.count) : data.count || 0,
+        typeof data.count === 'string'
+          ? parseInt(data.count, 10)
+          : (data.count ?? 0),
     };
   };
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const convertToRecruitItem = (data: RecruitData): RecruitItem => {
return {
url: data.url || '',
active: data.active || 1,
title: data.title || '',
jobName: data.jobName || '',
companyName: data.companyName || '',
locationName: data.locationName || null,
jobTypeName: data.jobTypeName || '',
experienceLevel: data.experienceLevel || '',
requiredEducationLevel: data.requiredEducationLevel || '',
closeType: data.closeType || '',
salary: data.salary || '',
id: String(data.id),
postTimestamp: data.postTimestamp || '',
postDate: data.postDate || '',
'expiration-timestamp': data['expiration-timestamp'] || '',
'expiration-date': data['expiration-date'] || '',
deadline: data.deadline || '',
count:
typeof data.count === 'string' ? parseInt(data.count) : data.count || 0,
};
};
const convertToRecruitItem = (data: RecruitData): RecruitItem => {
return {
url: data.url || '',
active: data.active ?? 1,
title: data.title || '',
jobName: data.jobName || '',
companyName: data.companyName || '',
locationName: data.locationName || null,
jobTypeName: data.jobTypeName || '',
experienceLevel: data.experienceLevel || '',
requiredEducationLevel: data.requiredEducationLevel || '',
closeType: data.closeType || '',
salary: data.salary || '',
id: String(data.id),
postTimestamp: data.postTimestamp || '',
postDate: data.postDate || '',
'expiration-timestamp': data['expiration-timestamp'] || '',
'expiration-date': data['expiration-date'] || '',
deadline: data.deadline || '',
count:
typeof data.count === 'string'
? parseInt(data.count, 10)
: (data.count ?? 0),
};
};
๐Ÿค– Prompt for AI Agents
In src/pages/home/components/HomeRecruit.tsx around lines 49 to 71, the
converter incorrectly treats active: data.active || 1 which turns a legitimate 0
into 1 and parseInt lacks a radix; change to use nullish coalescing for active
(active: data.active ?? 1) so only null/undefined default to 1, and when parsing
count explicitly pass a radix 10 (e.g. parseInt(data.count, 10)) while
preserving the existing fallback to 0 when count is missing or not a number.

Comment on lines 36 to 70
const toggleAdd = (id: number, isAdded: boolean) => {
if (isAdded) {
deleteTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setAdded((prev) => ({ ...prev, [id]: false }));
setToastMessage('ํ• ์ผ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
setShowToast(true);
setTimeout(() => setShowToast(false), 2500);
},
onError: () => {
alert('์ถ”๊ฐ€ ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
} else {
addTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setShowToast(true);
setAdded((prev) => ({ ...prev, [id]: true }));
setToastMessage('ํ• ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
setTimeout(() => {
setShowToast(false);
}, 2500);
},
onError: () => {
alert('๋‚ด ํ• ์ผ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ› ๏ธ Refactor suggestion

๋น ๋ฅธ ์—ฐํƒ€/์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ ๋ฐ ํƒ€์ž„์•„์›ƒ ๊ด€๋ฆฌ

์—ฐ์† ํด๋ฆญ ์‹œ add/delete๊ฐ€ ์ค‘์ฒฉ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ mutation์˜ isPending ์ƒํƒœ๋กœ ๋ฒ„ํŠผ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ , setTimeout์€ ํด๋ฆฐ์—…์œผ๋กœ ํ•ด์ œํ•˜์„ธ์š”.

๋‹ค์Œ ํŒจ์น˜ ์˜ˆ์‹œ:

-  const addTodoMutation = useCommunityAddTodoMutation();
-  const deleteTodoMutation = useDeleteCommunityTodosMutation();
+  const {
+    mutate: addMutate,
+    isPending: isAdding,
+  } = useCommunityAddTodoMutation();
+  const {
+    mutate: deleteMutate,
+    isPending: isDeleting,
+  } = useDeleteCommunityTodosMutation();
+  const isMutating = isAdding || isDeleting;
+  const toastTimers = useRef<number[]>([]);
...
-      deleteTodoMutation.mutate(
+      deleteMutate(
         { id },
         {
           onSuccess: () => {
             setAdded((prev) => ({ ...prev, [id]: false }));
             setToastMessage('ํ• ์ผ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
             setShowToast(true);
-            setTimeout(() => setShowToast(false), 2500);
+            const t = window.setTimeout(() => setShowToast(false), 2500);
+            toastTimers.current.push(t);
           },
...
-      addTodoMutation.mutate(
+      addMutate(
         { id },
         {
           onSuccess: () => {
             setShowToast(true);
             setAdded((prev) => ({ ...prev, [id]: true }));
             setToastMessage('ํ• ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
-            setTimeout(() => {
-              setShowToast(false);
-            }, 2500);
+            const t = window.setTimeout(() => setShowToast(false), 2500);
+            toastTimers.current.push(t);
           },
...
+  useEffect(() => {
+    return () => {
+      toastTimers.current.forEach((t) => clearTimeout(t));
+      toastTimers.current = [];
+    };
+  }, []);

๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”๋Š” ํ•˜๋‹จ ๋ฒ„ํŠผ ์˜์—ญ์—์„œ isMutating์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค(์•„๋ž˜ ์ฝ”๋ฉ˜ํŠธ ์ฐธ๊ณ ).

๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toggleAdd = (id: number, isAdded: boolean) => {
if (isAdded) {
deleteTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setAdded((prev) => ({ ...prev, [id]: false }));
setToastMessage('ํ• ์ผ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
setShowToast(true);
setTimeout(() => setShowToast(false), 2500);
},
onError: () => {
alert('์ถ”๊ฐ€ ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
} else {
addTodoMutation.mutate(
{ id },
{
onSuccess: () => {
setShowToast(true);
setAdded((prev) => ({ ...prev, [id]: true }));
setToastMessage('ํ• ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
setTimeout(() => {
setShowToast(false);
}, 2500);
},
onError: () => {
alert('๋‚ด ํ• ์ผ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
}
};
// Destructure mutate + pending states, track combined mutating status and toast timers
const {
mutate: addMutate,
isPending: isAdding,
} = useCommunityAddTodoMutation();
const {
mutate: deleteMutate,
isPending: isDeleting,
} = useDeleteCommunityTodosMutation();
const isMutating = isAdding || isDeleting;
const toastTimers = useRef<number[]>([]);
const toggleAdd = (id: number, isAdded: boolean) => {
if (isAdded) {
deleteMutate(
{ id },
{
onSuccess: () => {
setAdded((prev) => ({ ...prev, [id]: false }));
setToastMessage('ํ• ์ผ์ด ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
setShowToast(true);
const t = window.setTimeout(() => setShowToast(false), 2500);
toastTimers.current.push(t);
},
onError: () => {
alert('์ถ”๊ฐ€ ์ทจ์†Œ์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
} else {
addMutate(
{ id },
{
onSuccess: () => {
setShowToast(true);
setAdded((prev) => ({ ...prev, [id]: true }));
setToastMessage('ํ• ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
const t = window.setTimeout(() => setShowToast(false), 2500);
toastTimers.current.push(t);
},
onError: () => {
alert('๋‚ด ํ• ์ผ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์–ด์š”.');
},
}
);
}
};
useEffect(() => {
return () => {
toastTimers.current.forEach((t) => clearTimeout(t));
toastTimers.current = [];
};
}, []);

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

๐Ÿงน Nitpick comments (3)
src/hook/community/query/useGetHotPopularQuery.ts (3)

14-15: todoGroupId ํ•„์ˆ˜ ์ถ”๊ฐ€ โ€” API ์Šคํ‚ค๋งˆ/์†Œ๋น„์ฒ˜ ์ •ํ•ฉ์„ฑ ํ™•์ธ ๊ถŒ์žฅ

ํ˜„์žฌ queryFn์—์„œ ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ ์—†์ด ์„œ๋ฒ„ ์‘๋‹ต์„ ์‹ ๋ขฐํ•˜๋ฏ€๋กœ, ์„œ๋ฒ„๊ฐ€ todoGroupId๋ฅผ ๋ˆ„๋ฝํ•˜๋ฉด undefined๊ฐ€ UI๋กœ ํ˜๋Ÿฌ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜, ์ปค๋ฎค๋‹ˆํ‹ฐ HOT ๋ฆฌ์ŠคํŠธ๋Š” ์—ฌ์ „ํžˆ item.id๋กœ ๋„ค๋น„๊ฒŒ์ด์…˜ํ•œ๋‹ค๊ณ  ํ•˜๋‹ˆ, todoGroupId์˜ ์‹ค์ œ ์‚ฌ์šฉ ๋ชฉ์ (๊ทธ๋ฃน ID vs ์•„์ดํ…œ ID)์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. ์Šคํ‚ค๋งˆ๊ฐ€ ์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์ž„์‹œ๋กœ optional ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋Ÿฐํƒ€์ž„ ํŒŒ์‹ฑ(์˜ˆ: Zod)์œผ๋กœ ๋ณด์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์„ ํƒ์ง€ A(์ž„์‹œ ์™„์ถฉ):

-  todoGroupId: number;
+  todoGroupId?: number;

์›ํ•˜์‹œ๋ฉด Zod ์Šคํ‚ค๋งˆ/ํŒŒ์‹ฑ ์ฝ”๋“œ๋„ ์ œ์•ˆํ•ด ๋“œ๋ฆด๊ฒŒ์š”.


26-35: ์‘๋‹ต ์ œ๋„ค๋ฆญ/๊ฒ€์ฆ ์ ์šฉ์œผ๋กœ ์นจ๋ฌต ์‹คํŒจ ๋ฐฉ์ง€

๋น„์ •์ƒ ์‘๋‹ต ์‹œ ๋นˆ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์žฅ์• ๊ฐ€ ๊ฐ€๋ ค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Axios ์ œ๋„ค๋ฆญ๊ณผ ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ ๊ฒ€์ฆ์œผ๋กœ ์—๋Ÿฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ„ฐ๋œจ๋ ค React Query์˜ ์—๋Ÿฌ ํ”Œ๋กœ์šฐ๋ฅผ ํƒ€๋„๋ก ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

-  return useQuery<HotPopularItem[]>({
+  return useQuery<HotPopularItem[], Error>({
     queryKey: ['hotPopular', selectedJobName],
     enabled: !!selectedJobName,
     queryFn: async () => {
-      const res = await api.get('/v1/community/todos/popular', {
+      const { data } = await api.get<HotPopularApiResponse>('/v1/community/todos/popular', {
         params: { jobName: selectedJobName },
       });
-      const body = res.data as HotPopularApiResponse;
-      return Array.isArray(body?.data) ? body.data : [];
+      if (!Array.isArray(data?.data)) {
+        throw new Error('Invalid response shape: /v1/community/todos/popular');
+      }
+      return data.data;
     },
     staleTime: 1000 * 60 * 5,
     refetchOnWindowFocus: false,
   });

5-10: imageUrl null ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ ๋ฐ ํƒ€์ž… ์ •ํ•ฉ์„ฑ

๊ด€๋ จ ์Šคํ‚ค๋งˆ์—์„œ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๊ฐ€ nullable๋กœ ๋ฐ”๋€ ๋งฅ๋ฝ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ๋™์ผํ•˜๋‹ค๋ฉด imageUrl๋„ null์ด ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์œผ๋‹ˆ ํƒ€์ž…/์†Œ๋น„์ฒ˜ ์ •ํ•ฉ์„ฑ์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. ํ•„์š” ์‹œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ™•์žฅํ•˜๊ฑฐ๋‚˜, ์†Œ๋น„ ์ธก์—์„œ ํด๋ฐฑ์„ ์ ์šฉํ•˜์„ธ์š”.

-  imageUrl: string;
+  imageUrl: string | null;
๐Ÿ“œ Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 7453db5 and 516a051.

๐Ÿ“’ Files selected for processing (1)
  • src/hook/community/query/useGetHotPopularQuery.ts (1 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (1)
src/pages/community/components/CommunityContents.tsx (1)

52-87: ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€: isPending์œผ๋กœ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™” ๋ฐ ํ† ์ŠคํŠธ ํƒ€์ด๋จธ ํด๋ฆฐ์—…

์—ฐ์† ํด๋ฆญ ์‹œ add/delete๊ฐ€ ์ค‘๋ณต ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ mutation์˜ isPending์„ ์ด์šฉํ•ด ๋ฒ„ํŠผ์„ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ณ , setTimeout์€ ref๋กœ ๊ด€๋ฆฌํ•ด ํด๋ฆฐ์—…ํ•˜์„ธ์š”.

์˜ˆ์‹œ ํŒจ์น˜:

-  const addTodoMutation = useCommunityAddTodoMutation();
-  const deleteTodoMutation = useDeleteCommunityTodosMutation();
+  const { mutate: addMutate, isPending: isAdding } = useCommunityAddTodoMutation();
+  const { mutate: deleteMutate, isPending: isDeleting } = useDeleteCommunityTodosMutation();
+  const isMutating = isAdding || isDeleting;
+  const toastTimerRef = useRef<number | null>(null);
...
-      deleteTodoMutation.mutate(
+      deleteMutate(
...
-      addTodoMutation.mutate(
+      addMutate(
...
-            setTimeout(() => setShowToast(false), 2500);
+            toastTimerRef.current = window.setTimeout(() => setShowToast(false), 2500);
...
+  useEffect(() => () => {
+    if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
+  }, []);

๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”:

-              <button
+              <button
                 type="button"
-                onClick={() => toggleAdd(post.id, isAdded, post.description)}
+                onClick={() => toggleAdd(post.id, isAdded, post.description)}
+                disabled={isMutating}
โ™ป๏ธ Duplicate comments (2)
src/pages/jobDetail/components/TabContent/TreeContents.tsx (1)

29-31: ํƒ€์ด๋จธ ์ •๋ฆฌ ๋ˆ„๋ฝ ๊ฐ€๋Šฅ์„ฑ

์–ธ๋งˆ์šดํŠธ ์‹œ ํƒ€์ด๋จธ ํด๋ฆฐ์—…์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. Sprout ์ฝ”๋ฉ˜ํŠธ์™€ ๋™์ผ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ ๊ถŒ์žฅ.

src/pages/otherTodoList/components/OtherTodoCard.tsx (1)

37-72: ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ ๋ฐ ํƒ€์ž„์•„์›ƒ ํด๋ฆฐ์—…

์ด์ „ ์ฝ”๋ฉ˜ํŠธ์™€ ๋™์ผํ•˜๊ฒŒ isPending ๊ธฐ๋ฐ˜ ๋น„ํ™œ์„ฑํ™”์™€ ํƒ€์ด๋จธ ํด๋ฆฐ์—…์„ ์ ์šฉํ•˜์„ธ์š”. ์—ฐ์† ํด๋ฆญ ์‹œ ์„œ๋ฒ„์— ์ค‘๋ณต ์š”์ฒญ์ด ๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงน Nitpick comments (15)
src/utils/amplitude.ts (6)

9-11: SSR ์•ˆ์ „์„ฑ ๋ฐ ์ „์—ญ ์ฐธ์กฐ ์ผ๊ด€ํ™”: window.location ์‚ฌ์šฉ ๊ถŒ์žฅ

location.pathname ๋Œ€์‹  window.location.pathname์„ ์‚ฌ์šฉํ•ด ์ „์—ญ ์ฐธ์กฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ•˜๊ณ , SSR ๊ฐ€๋“œ์™€์˜ ์ผ๊ด€์„ฑ์„ ๋งž์ถ”์„ธ์š”.

-    const currentPath = location.pathname;
+    const currentPath = window.location.pathname;

12-21: source_page ๊ฐ’ ํ‘œ์ค€ํ™” ๋ฐ ๋ˆ„๋ฝ ๋ผ์šฐํŠธ ๋ณด์™„

Sprout/Tree ์ปดํฌ๋„ŒํŠธ๋Š” ๋ณดํ†ต jobDetail ๊ฒฝ๋กœ์—์„œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ๋งคํ•‘์€ '/jobinfo'๋งŒ ์ปค๋ฒ„ํ•˜์—ฌ ๋™์ผ ๊ธฐ๋Šฅ์ด ํŽ˜์ด์ง€์— ๋”ฐ๋ผ 'jobinfo/' vs '/jobDetail/...'๋กœ ์„ž์—ฌ ๊ธฐ๋ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ธก์ • ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด jobDetail๋„ ๋ช…์‹œ ๋งคํ•‘ํ•˜๊ณ , ์Šฌ๋ž˜์‹œ/๋Œ€์†Œ๋ฌธ์ž ๊ทœ์น™์„ ํ†ต์ผํ•˜์„ธ์š”.

-    if (currentPath.includes('/community')) {
+    if (currentPath.includes('/community')) {
       sourcePage = 'community/';
-    } else if (currentPath.includes('/jobinfo')) {
+    } else if (currentPath.includes('/jobinfo')) {
       sourcePage = 'jobinfo/';
+    } else if (currentPath.includes('/jobdetail')) {
+      sourcePage = 'jobdetail/';
     } else if (currentPath.includes('/otherslist')) {
       sourcePage = 'otherslist/';
     } else {
       sourcePage = currentPath;
     }

23-28: ์ด๋ฒคํŠธ ์‹œ๋งจํ‹ฑ ๋ถˆ์ผ์น˜: source_method 'copy_btn' โ†’ ์‹ค์ œ๋Š” ์ถ”๊ฐ€ ๋ฒ„ํŠผ

ํ˜„์žฌ ์ด๋ฒคํŠธ๋Š” '๋‚ด ํ• ์ผ์— ์ถ”๊ฐ€' ํ๋ฆ„์ธ๋ฐ source_method๊ฐ€ copy_btn์œผ๋กœ ๊ธฐ๋ก๋ฉ๋‹ˆ๋‹ค. ๋ถ„์„ ํ˜ผ์„ ์„ ํ”ผํ•˜๋ ค๋ฉด ๋ฉ”์„œ๋“œ ๊ฐ’์„ ๊ต์ •ํ•˜๊ฑฐ๋‚˜, ํด๋ฆญ/์„ฑ๊ณต ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•ด ๊ธฐ๋กํ•˜์„ธ์š”.

-      source_method: 'copy_btn',
+      source_method: 'add_btn',

๋Œ€์•ˆ: ํด๋ฆญ ์‹œ 'todo_import_click', ์„ฑ๊ณต ์‹œ 'todo_import_success'๋ฅผ ๋”ฐ๋กœ ๋ณด๋‚ด๋Š” ํŒจํ„ด์œผ๋กœ ๋ถ„๋ฆฌ.


23-28: ๋ถ„์„ ์‹๋ณ„์ž ํ™•์žฅ: todo_id ์ „๋‹ฌ ๊ณ ๋ ค

๊ธธ์ด(todo_length)๋งŒ์œผ๋กœ๋Š” ํ•ญ๋ชฉ ์‹๋ณ„์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ์ธ์ •๋ณด ๋…ธ์ถœ์„ ํ”ผํ•˜๋ฉด์„œ๋„ ๋ถ„์„ ํ’ˆ์งˆ์„ ๋†’์ด๋ ค๋ฉด ID๋ฅผ ์ถ”๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „๋‹ฌํ•˜๋Š” ํ˜•ํƒœ๋กœ ์œ ํ‹ธ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ํ™•์žฅํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ:

-export const trackTodoImport = (todoTitle: string) => {
+export const trackTodoImport = (todoTitle: string, options?: { todoId?: number }) => {
...
-    window.amplitude.track('todo_import', {
+    window.amplitude.track('todo_import', {
       source_method: 'add_btn',
       source_page: sourcePage,
       todo_length: todoTitle.length,
+      todo_id: options?.todoId,
       timestamp: new Date().toISOString(),
     });

ํ˜ธ์ถœ๋ถ€์—์„œ ID๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ํ•จ๊ป˜ ์ „๋‹ฌ.


29-30: prod ์ฝ˜์†” ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ

ํ”„๋กœ๋•์…˜์—์„œ ๋ถˆํ•„์š”ํ•œ ์ฝ˜์†” ์ถœ๋ ฅ์€ ์ง€์–‘ํ•˜์„ธ์š”. ๋นŒ๋“œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ฐ€๋“œํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ๋ฅผ ์ œ๊ฑฐํ•ด ์ฃผ์„ธ์š”.

-    console.log('Amplitude event sent: todo_import');
+    if (process.env.NODE_ENV !== 'production') {
+      console.log('Amplitude event sent: todo_import');
+    }

3-8: window.amplitude ํƒ€์ž… ์„ ์–ธ ๋ˆ„๋ฝ ๊ฐ€๋Šฅ์„ฑ

window.amplitude๋Š” ๊ธฐ๋ณธ DOM ํƒ€์ž…์— ์—†์–ด์„œ TS ์—๋Ÿฌ๊ฐ€ ๋‚  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธ€๋กœ๋ฒŒ ์„ ์–ธ์„ ์ถ”๊ฐ€ํ•ด ํƒ€์ž…์„ ๋ช…์‹œํ•˜์„ธ์š”.

์•„๋ž˜๋ฅผ ๋ณธ ํŒŒ์ผ ์ตœ์ƒ๋‹จ(๋˜๋Š” .d.ts) ์–ด๋”˜๊ฐ€์— ์ถ”๊ฐ€:

declare global {
  interface Window {
    amplitude?: { track: (event: string, props?: Record<string, any>) => void };
  }
}
export {};
src/pages/jobDetail/components/TabContent/SproutContent.tsx (3)

21-33: ์ด๋ฒคํŠธ๋Š” ์„ฑ๊ณต ์‹œ์ ์— ๊ธฐ๋ก

ํ˜„์žฌ ํด๋ฆญ ์งํ›„์— ์ถ”์ ํ•˜๊ณ  ์žˆ์–ด ์‹คํŒจ ์ผ€์ด์Šค๋„ ์„ฑ๊ณต์œผ๋กœ ์ง‘๊ณ„๋ฉ๋‹ˆ๋‹ค. onSuccess์—์„œ ํŠธ๋ž˜ํ‚นํ•˜๋„๋ก ์ด๋™ํ•˜๊ฑฐ๋‚˜, ํด๋ฆญ/์„ฑ๊ณต์„ ๋ถ„๋ฆฌ ์ด๋ฒคํŠธ๋กœ ๊ธฐ๋กํ•˜์„ธ์š”.

-  const handleAdd = (jobTodoId: number, title: string) => {
+  const handleAdd = (jobTodoId: number, title: string) => {
     if (completedId.has(jobTodoId)) return;

     setClickedId(jobTodoId);
-    trackTodoImport(title); // Amplitude ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น
     mutate(jobTodoId, {
       onSuccess: () => {
+        trackTodoImport(title); // ์„ฑ๊ณต ์‹œ์  ํŠธ๋ž˜ํ‚น
         setCompletedId((prev) => new Set(prev).add(jobTodoId));
         setShowToast(true);
         setTimeout(() => setShowToast(false), 2000);
       },
       onSettled: () => setClickedId(null),
     });
   };

29-31: ํƒ€์ด๋จธ ์ •๋ฆฌ ๋ˆ„๋ฝ ๊ฐ€๋Šฅ์„ฑ

์–ธ๋งˆ์šดํŠธ ์‹œ setTimeout ํด๋ฆฐ์—…์ด ์—†์–ด ๋ฉ”๋ชจ๋ฆฌ/๊ฒฝ๊ณ  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ref๋กœ ํƒ€์ด๋จธ๋ฅผ ๋ณด๊ด€ํ•˜๊ณ  useEffect cleanup์—์„œ ์ •๋ฆฌํ•˜์„ธ์š”.

์ถ”๊ฐ€ ์ฝ”๋“œ(์ปดํฌ๋„ŒํŠธ ์ƒ๋‹จ์—):

const toastTimerRef = useRef<number | null>(null);

๋ณ€๊ฒฝ:

- setTimeout(() => setShowToast(false), 2000);
+ toastTimerRef.current = window.setTimeout(() => setShowToast(false), 2000);

ํด๋ฆฐ์—…:

useEffect(() => () => {
  if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
}, []);

8-11: ๋ฏธ์‚ฌ์šฉ prop ์ •๋ฆฌ

SproutContentProps์˜ jobId๊ฐ€ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ถˆํ•„์š”ํ•œ prop์€ ์ œ๊ฑฐํ•ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋‹จ์ˆœํ™”ํ•˜์„ธ์š”.

src/pages/jobDetail/components/TabContent/TreeContents.tsx (1)

21-33: ์ด๋ฒคํŠธ๋Š” ์„ฑ๊ณต ์‹œ์ ์— ๊ธฐ๋ก

Sprout๊ณผ ๋™์ผํ•˜๊ฒŒ ์„ฑ๊ณต ์‹œ์ ์— ๊ธฐ๋กํ•˜๋„๋ก ์ด๋™ํ•˜์„ธ์š”.

-    trackTodoImport(title); // Amplitude ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น
     mutate(jobTodoId, {
       onSuccess: () => {
+        trackTodoImport(title); // ์„ฑ๊ณต ์‹œ์  ํŠธ๋ž˜ํ‚น
         setCompletedId((prev) => new Set(prev).add(jobTodoId));
         setShowToast(true);
         setTimeout(() => setShowToast(false), 2000);
       },
src/pages/community/components/CommunityContents.tsx (2)

69-70: ์ด๋ฒคํŠธ ์‹œ์  ์กฐ์ • ๋˜๋Š” ์ด๋ฒคํŠธ๋ช… ๋ถ„๋ฆฌ

์ถ”๊ฐ€ ์„ฑ๊ณต ์ „ ํŠธ๋ž˜ํ‚น๋˜์–ด ์‹คํŒจ ๊ฑด๋„ ์„ฑ๊ณต์œผ๋กœ ์ง‘๊ณ„๋ฉ๋‹ˆ๋‹ค. onSuccess์—์„œ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜, ํด๋ฆญ/์„ฑ๊ณต ์ด๋ฒคํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

-      trackTodoImport(todoTitle); // Amplitude ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น
+      // onSuccess ๋‚ด๋ถ€๋กœ ์ด๋™ ๊ถŒ์žฅ ๋˜๋Š” ๋ณ„๋„ 'todo_import_click' ์ด๋ฒคํŠธ๋กœ ๋ถ„๋ฆฌ

136-144: ๊ธด ๋ณธ๋ฌธ(description) ๋Œ€์‹  ์งง์€ ์‹๋ณ„์ž ์‚ฌ์šฉ ๊ฒ€ํ† 

todoTitle๋กœ post.description์„ ์ „๋‹ฌํ•˜๋ฉด ๊ธธ์ด ์ธก์ •์ด ๊ณผ๋Œ€ ๊ธฐ๋ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ๋ชฉ(post.name) ๋˜๋Š” ์•ž๋ถ€๋ถ„๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ์žฌ๊ฒ€ํ† ํ•˜์„ธ์š”.

- onClick={() => toggleAdd(post.id, isAdded, post.description)}
+ onClick={() => toggleAdd(post.id, isAdded, post.name)}
src/pages/otherTodoList/components/OtherTodoCard.tsx (3)

149-157: ์ด๋ฒคํŠธ๋Š” ์„ฑ๊ณต ์‹œ์ ์— ๊ธฐ๋ก

์„ฑ๊ณต/์‹คํŒจ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ ค๋ฉด onSuccess์—์„œ ํŠธ๋ž˜ํ‚นํ•˜๊ฑฐ๋‚˜ ์ด๋ฒคํŠธ๋ฅผ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

-      trackTodoImport(todoTitle); // Amplitude ์ด๋ฒคํŠธ ํŠธ๋ž˜ํ‚น
       addTodoMutation.mutate(
         { id },
         {
           onSuccess: () => {
+            trackTodoImport(todoTitle); // ์„ฑ๊ณต ์‹œ์  ํŠธ๋ž˜ํ‚น
             setShowToast(true);

114-116: ๋ฆฌ์ŠคํŠธ key์— index ์‚ฌ์šฉ ์ง€์–‘

index key๋Š” ์ถ”๊ฐ€/์‚ญ์ œ ํ† ๊ธ€ ์‹œ ์ž˜๋ชป๋œ ์žฌ์‚ฌ์šฉ์„ ์œ ๋ฐœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณ ์œ  ID๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.

-              key={index}
+              key={item.todoId}

118-124: ํด๋ฆญ ๊ฐ€๋Šฅ UI์™€ ๋™์ž‘ ๋ถˆ์ผ์น˜

์ฒดํฌ ๋ฐ•์Šค ์˜์—ญ์— cursor-pointer๊ฐ€ ์žˆ์œผ๋‚˜ onClick ๋™์ž‘์ด ์—†์Šต๋‹ˆ๋‹ค. UX ํ˜ผ์„ ์„ ํ”ผํ•˜๋ ค๋ฉด ํฌ์ธํ„ฐ ์ œ๊ฑฐ ๋˜๋Š” ํด๋ฆญ ๋™์ž‘์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

-                  className={`flex h-[30px] w-[30px] cursor-pointer items-center justify-center rounded-[8px] border ${
+                  className={`flex h-[30px] w-[30px] items-center justify-center rounded-[8px] border ${
๐Ÿ“œ Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 516a051 and a5186aa.

๐Ÿ“’ Files selected for processing (5)
  • src/pages/community/components/CommunityContents.tsx (4 hunks)
  • src/pages/jobDetail/components/TabContent/SproutContent.tsx (3 hunks)
  • src/pages/jobDetail/components/TabContent/TreeContents.tsx (3 hunks)
  • src/pages/otherTodoList/components/OtherTodoCard.tsx (3 hunks)
  • src/utils/amplitude.ts (1 hunks)
๐Ÿงฐ Additional context used
๐Ÿงฌ Code graph analysis (4)
src/pages/jobDetail/components/TabContent/SproutContent.tsx (1)
src/utils/amplitude.ts (1)
  • trackTodoImport (3-31)
src/pages/jobDetail/components/TabContent/TreeContents.tsx (1)
src/utils/amplitude.ts (1)
  • trackTodoImport (3-31)
src/pages/community/components/CommunityContents.tsx (1)
src/utils/amplitude.ts (1)
  • trackTodoImport (3-31)
src/pages/otherTodoList/components/OtherTodoCard.tsx (3)
src/hook/community/useCommunityAddTodoMutation.ts (1)
  • useCommunityAddTodoMutation (16-51)
src/hook/community/useDeleteCommunityTodos.ts (1)
  • useDeleteCommunityTodosMutation (8-38)
src/utils/amplitude.ts (1)
  • trackTodoImport (3-31)
๐Ÿ”‡ Additional comments (1)
src/pages/otherTodoList/components/OtherTodoCard.tsx (1)

141-145: ๋ถ๋งˆํฌ ์นด์šดํŠธ NaN ๋ฐฉ์ง€ ์ฒ˜๋ฆฌ๋Š” OK

{item.saveCount || 0}๋กœ ์•ˆ์ •์ ์œผ๋กœ ๋ Œ๋”๋ง๋˜๋Š” ์  ์ข‹์Šต๋‹ˆ๋‹ค.

Copy link
Member

@Chasyuss Chasyuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ํ™•์ธ์ด์š”...!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants